feat(auth): SEP-10-style challenge-transaction signature scheme - #138
Merged
Merged
Conversation
Add a 'sep0010' verification scheme to POST /auth/verify so wallets that only expose transaction signing (mobile Lobstr over WalletConnect stellar_signXDR) can authenticate, not just wallets that sign arbitrary messages. POST /auth/nonce now also returns `challengeXdr`: an unsigned SEP-10-style challenge transaction whose source is the user's own wallet at sequence 0 (built on an Account seeded at "-1") with a single manageData operation whose value equals the SHA-256 hash already stored on the nonce row. The wallet signs this XDR and returns it as `signedXdr` with `signatureType: 'sep0010'`. Verification asserts the signed transaction is exactly the issued challenge — matching source, sequence 0, single manageData op with the expected name and a value equal to the stored challenge hash, non-expired timebounds — and carries a valid wallet signature over the transaction hash (the network passphrase is bound implicitly through tx.hash()). Deliberate deviation from strict SEP-10: the challenge is server-issued but not server-signed, so no server SIGNING_KEY secret is introduced. Forgery/replay is already prevented by the existing single-use nonce row, the stored message_hash binding, and the timebounds. Strict SEP-10 (server co-signature + WEB_AUTH_DOMAIN) remains a documented follow-up. The legacy raw/sep0043/envelope message schemes are unchanged. - nonce-response.dto: add challengeXdr - verify-request.dto: add 'sep0010' to signatureType; add optional signedXdr; make signature conditional (not required for sep0010) - tests: real-SDK round-trip spec (happy path + tamper/expiry/unsigned/ wrong-source/wrong-name/consumed-nonce), extend existing mock
Merged
14 of 15 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🔗 Related Issue
No API-side issue. This unblocks real wallet-signature JWT auth in the mobile app (a follow-up StepFi-App PR wires the client flow and closes the app issue). This PR is the API prerequisite it depends on.
🔖 Title
Add a
sep0010challenge-transaction signature scheme toPOST /auth/verify.📝 Description
POST /auth/verifypreviously only accepted an Ed25519 signature over message bytes (raw/sep0043/envelope). Mobile Lobstr over WalletConnect only exposesstellar_signXDR— it cannot sign arbitrary messages — so mobile clients could not produce a real signature and were stuck on mock tokens.This adds a SEP-10-style scheme: the wallet signs a server-issued challenge transaction (which both Lobstr and Freighter already support) instead of a message.
POST /auth/noncenow also returnschallengeXdr: an unsigned challenge transaction whose source is the user's own wallet at sequence 0 (built on anAccountseeded at"-1"), carrying a singlemanageDataoperation whose value equals the SHA-256 hash already stored on the nonce row. The client signs this XDR and submits it back assignedXdrwithsignatureType: "sep0010".Verification asserts the signed transaction is the challenge we issued:
=== wallet, sequence=== "0"manageDataop with the expected name===the stored challenge hash (the binding)tx.hash()that verifies against the wallet key (the network passphrase is bound implicitly, since the hash only matches for the correct network)Deliberate deviation from strict SEP-10: the challenge is server-issued but not server-signed, so no new server
SIGNING_KEYsecret is introduced. Forgery/replay is already prevented by the existing single-use nonce row (atomic claim), the storedmessage_hashbinding, and the timebounds. A.build()ed transaction with a source account at sequence 0 can never be submitted to the network, so it is a pure auth artifact. Strict SEP-10 (server co-signature +WEB_AUTH_DOMAIN) remains a documented follow-up if ever required.The legacy
raw/sep0043/envelopemessage schemes are unchanged.🔄 Changes Made
auth.service: build the unsigned challenge tx ingenerateNonce; verify the signed challenge in a newsep0010branch ofverifySignature(reusesAUTH_SIGNATURE_INVALID/AUTH_CHALLENGE_MISMATCH/AUTH_NONCE_EXPIRED)nonce-response.dto: addchallengeXdrverify-request.dto: add'sep0010'tosignatureType; add optionalsignedXdr; makesignatureconditional (not required forsep0010)🗒️ Additional Notes
npm run build,npm run lint:ci(0 warnings),npm test(485 passing).sep0010spec deliberately opts out of the repo's manualtest/__mocks__/stellar-sdk.js(viajest.unmock) so it exercises the genuine sign→verify round-trip with a throwawayKeypair— the end-to-end proof that a signXDR-only wallet can now authenticate.